#!/usr/bin/env python
"""
============================
timesched documentation
============================
Introduction
============================
This package comes with a useful *Scheduler* class. This class permits to create a single-thread scheduler. You can enter or cancel events (function that must start in a determined time with a certain recursion). Recursion logic is designed for use with rrule or rruleset objects (`dateutil <http://labix.org/python-dateutil>`_ package). RRule is a powerful recursion algorithm class. 

Scheduler class - Usage
============================
First of all import scheduler module

    >>> from timesched import *

This is a sample function

    >>> def action(s):
    ...     print "Hello %s!" % (s,)

create an instance

    >>> s=Scheduler()

enter new scheduled event

    >>> marioId=s.enter(action,args=("Mario",))

start the scheduler

    >>> s.start()

you can add or cancel other events even if it is started

    >>> s.cancel(marioId)
    >>> johnId=s.enter(action,args=("John",))
    >>> sleep(1)
    Hello John!

stop the scheduler (you can't restart again! You need a new instance of Scheduler)

    >>> s.stop()

if the event doesn't have a valid recursion date it returns None

    >>> print s.enter(action,args=("Mario",),
    ...               lastTime=datetime(1980,03,21),
    ...               when=rrule(YEARLY,dtstart=datetime(1977,03,21),count=4))
    None

Events with results
=====================
If your function returns values you can optionally pass a results queue.

    >>> import Queue
    >>> def hello():
    ...     return "hello!"

    >>> res=Queue.Queue()

    >>> s=Scheduler()
    >>> s.start()
    >>> s=s.enter(hello,results=res)
    >>> print res.get() # wait the result
    hello!

Events samples
===============

    >>> s=Scheduler()

The event starts now

    >>> event=s.enter(action,
    ...               args=("Mario",))

The event starts every 4 seconds

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=rrule(SECONDLY,interval=4))

The event starts every 10 minutes only twice

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=rrule(MINUTELY,interval=10,count=2))

The event starts every week until 2010

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=rrule(WEEKLY,until=datetime(2010,1,1)))

The event starts every 2 days in june and july

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=rrule(DAILY,interval=2,bymonth=(6,7)))

Every Friday the 13th, 4 occurrences

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=rrule(DAILY,byweekday=FR, bymonthday=13))

The event starts on 2009/09/21 without recursion

    >>> event=s.enter(action,
    ...               args=("Mario",),
    ...               when=datetime(2009,9,21))

You can also use rruleset object: a set of rrule and datetime objects. 

*rruleset.rrule(rrule)* 
    add this rrule to rruleset
*rruleset.rdate(dt)* 
    add this datetime to rruleset
*rruleset.exrule(rrule)*
    exclude this rrule from rruleset
*rruleset.exdate(dt)* 
    exclude this date from rruleset

See dateutil documentation for details. http://labix.org/python-dateutil

Errors
===============
You can optionally pass a log function. So you can freely choose the way to track your functions errors.
The log function arguments are:

*event*
    the event who raise the error
*t*
    type of exception (sys.exc_info[0])
*v*
    value of exception (sys.exc_info[1])

The default logger prints on console.

    >>> def bugFunction():
    ...     raise ValueError,"Function failed!!"

create an instance

    >>> s=Scheduler()
    >>> s.start()
    >>> event=s.enter(bugFunction) 
    >>> sleep(1)           #doctest: +ELLIPSIS
    Error: Event:... - <type 'exceptions.ValueError'>
    >>> s.stop()

"""
from datetime import datetime, timedelta
from dateutil.rrule import *
from time import sleep
import threading
from heapq import heapify,heappush,heappop
import sys

class Logger(object):
    """default log scheduler error:

use this as a prototype for your own logging object"""
    def __init__(self):
        """create the logger"""
    def __call__(self,event,t,v):
        """log"""
        eventid=id(event)
        print "Error: Event:%(eventid)s - %(t)s" % locals()

class SchedulerError(Exception):
    """bad scheduler api usage"""

class Scheduler(threading.Thread):
    """thread safe scheduler based on datetime.rrule"""
    def __init__(self,timeResolution=0.5,now=datetime.now,logErrors=Logger()):
        """timeResolution: number of second between every cycle
now: function used for determine what time is it
logErrors: callback function called when functions scheduled raises an exception. The default logger prints errors on console
"""
        threading.Thread.__init__(self)
        self.timeResolution=timeResolution
        self.now=now
        self.logErrors=logErrors
        self.setDaemon(1)
        self._queue =  []
        self._stop=False
        self.lockQueue=threading.Lock()
    def start(self):
        """start the thread: use stop to teminate"""
        threading.Thread.start(self)

    def stop(self):
        """stop the thread and terminate the scheduler"""
        self._stop=True
        self.join() # waiting the end

    def enter(self, action,
                    args=(), kwds={},
                    when=None, lastTime=None,
                    results=None,
                    priority=0):
        """Enter arguments - thread safe

action:
    it's mandatory (must be a callable)
args:
    optional tuple of arguments passed to action
kwds:
    optional keyword arguments
when:
    either a datetime, rrule or rruleset class instance
lastTime
    must be a datetime instance. It is used to compute the first occurrence. If it is None will be used the when._dtstart attribute.
    If recursion results is None the function exits with None
results
    a queue instance or None. The result of action is put in the queue (if it is not None)
priority:
    the less is the best :-)
"""
        if not callable(action):
            raise SchedulerError,"Action must be a callable"

        if results:
            if not hasattr(results,"put") and callable(results.put):
                raise SchedulerError,"results must have a put method"

        if not when: # ignoring lastTime
            time = self.now()
            recursion = None
        elif isinstance(when,datetime):
            recursion = None
            if lastTime and when <= lastTime:
                time = None
            else:
                time = when
        elif isinstance(when,rrule) or isinstance(when,rruleset):
            recursion = when
            if lastTime:
                time = when.after(lastTime)
            else:
                time = when[0]
        else:
            raise SchedulerError,"'when' must be a datetime, rrule or rruleset instance"
            
        if not time: return None # recursion = None
        
        # push the event
        event = [time, priority, recursion, action, args, kwds, results]
        self.lockQueue.acquire()
        try:
            heappush(self._queue,event)
        finally:
            self.lockQueue.release()
        return event # return the ID

    def cancel(self, event):
        """Cancel event - thread safe

if event not exists it raises ValueError"""
        self.lockQueue.acquire()
        try:
            self._queue.remove(event)
            heapify(self._queue)
        finally:
            self.lockQueue.release()

    def run(self):
        """called by start method in a new thread - private method: DO NOT CALL!!!"""
        self._stop=False
        doAction=False
        while not self._stop:
            if doAction:
                doAction=False
                try:
                    result = action(*args,**kwds)
                    if results:
                        results.put(result)
                except:
                    t,v=sys.exc_info()[:2] # manage exceptions
                    self.logErrors(event,t,v) 
            else:
                sleep(self.timeResolution) # let other threads to run (and queue to be modified)
            self.lockQueue.acquire()
            now = self.now()
            try:
                try:
                    eventTime=self._queue[0][0]
                except IndexError:
                    continue
                if now >= eventTime:
                    event=heappop(self._queue)
                    time, priority, recursion, action, args, kwds, results = event
                    doAction=True
                    #recursion
                    if recursion:
                        nextTime=recursion.after(now) # recursion is an rrule or rruleset instance
                        if nextTime:
                            event[0]=nextTime # change the current event time
                            heappush(self._queue,event) # push recursion
            finally:
                self.lockQueue.release()


#~ if __name__ == "__main__":
    #~ import doctest
    #~ doctest.testmod()
    

